"use strict";
(() => {
  var __create = Object.create;
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __getProtoOf = Object.getPrototypeOf;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
    get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
  }) : x)(function(x) {
    if (typeof require !== "undefined") return require.apply(this, arguments);
    throw Error('Dynamic require of "' + x + '" is not supported');
  });
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
    // If the importer is in node compatibility mode or this is not an ESM
    // file that has been converted to a CommonJS file using a Babel-
    // compatible transform (i.e. "__esModule" has not been set), then set
    // "default" to the CommonJS "module.exports" for node compatibility.
    isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
    mod
  ));
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);

  // src/front-end-module/libs/show-overlay.mjs
  function showOverlay(options = {}) {
    const {
      content = "",
      title = "",
      icon = "",
      type = "info",
      showDismiss,
      autoClose = 5,
      time = true
    } = options;
    const overlayContainerId = "uib-info-overlay";
    let overlayContainer = document.getElementById(overlayContainerId);
    if (!overlayContainer) {
      overlayContainer = document.createElement("div");
      overlayContainer.id = overlayContainerId;
      document.body.appendChild(overlayContainer);
      console.log(">> SHOW OVERLAY >>", options, document.getElementById(overlayContainerId));
    }
    const entryId = "overlay-entry-".concat(Date.now(), "-").concat(Math.random().toString(36).substr(2, 9));
    const overlayEntry = document.createElement("div");
    overlayEntry.id = entryId;
    overlayEntry.style.marginBottom = "0.5rem";
    const typeStyles = {
      info: {
        iconDefault: "\u2139\uFE0F",
        titleDefault: "Information",
        color: "hsl(188.2deg 77.78% 40.59%)"
      },
      success: {
        iconDefault: "\u2705",
        titleDefault: "Success",
        color: "hsl(133.7deg 61.35% 40.59%)"
      },
      warning: {
        iconDefault: "\u26A0\uFE0F",
        titleDefault: "Warning",
        color: "hsl(35.19deg 84.38% 62.35%)"
      },
      error: {
        iconDefault: "\u274C",
        titleDefault: "Error",
        color: "hsl(2.74deg 92.59% 62.94%)"
      }
    };
    const currentTypeStyle = typeStyles[type] || typeStyles.info;
    const shouldShowDismiss = showDismiss !== void 0 ? showDismiss : autoClose === null;
    const iconHtml = icon || currentTypeStyle.iconDefault;
    const titleText = title || currentTypeStyle.titleDefault;
    let timeHtml = "";
    if (time) {
      const now = /* @__PURE__ */ new Date();
      const year = now.getFullYear();
      const month = String(now.getMonth() + 1).padStart(2, "0");
      const day = String(now.getDate()).padStart(2, "0");
      const hours = String(now.getHours()).padStart(2, "0");
      const minutes = String(now.getMinutes()).padStart(2, "0");
      const seconds = String(now.getSeconds()).padStart(2, "0");
      const timestamp = "".concat(year, "-").concat(month, "-").concat(day, " ").concat(hours, ":").concat(minutes, ":").concat(seconds);
      timeHtml = '<div class="uib-overlay-time" style="font-size: 0.8em; color: var(--text3, #999); margin-left: auto; margin-right: '.concat(shouldShowDismiss ? "0.5rem" : "0", ';">').concat(timestamp, "</div>");
    }
    overlayEntry.innerHTML = /* html */
    '\n        <div class="uib-overlay-entry" style="--callout-color:'.concat(currentTypeStyle.color, ';">\n            <div class="uib-overlay-header">\n                <div class="uib-overlay-icon">').concat(iconHtml, '</div>\n                <div class="uib-overlay-title">').concat(titleText, "</div>\n                ").concat(timeHtml, "\n                ").concat(shouldShowDismiss ? '<button class="uib-overlay-dismiss" data-entry-id="'.concat(entryId, '" title="Close">\xD7</button>') : "", '\n            </div>\n            <div class="uib-overlay-content">\n                ').concat(content, "\n            </div>\n        </div>\n    ");
    if (overlayContainer.children.length > 0) {
      overlayContainer.insertBefore(overlayEntry, overlayContainer.firstChild);
    } else {
      overlayContainer.appendChild(overlayEntry);
    }
    const closeOverlayEntry = () => {
      const entry = document.getElementById(entryId);
      if (!entry) return;
      entry.style.animation = "slideOut 0.3s ease-in";
      setTimeout(() => {
        if (entry.parentNode) {
          entry.remove();
        }
      }, 300);
    };
    const dismissBtn = overlayEntry.querySelector(".uib-overlay-dismiss");
    if (dismissBtn) {
      dismissBtn.addEventListener("click", closeOverlayEntry);
    }
    let autoCloseTimer = null;
    if (autoClose !== null && autoClose > 0) {
      autoCloseTimer = setTimeout(closeOverlayEntry, autoClose * 1e3);
    }
    return {
      close: () => {
        if (autoCloseTimer) {
          clearTimeout(autoCloseTimer);
        }
        closeOverlayEntry();
      },
      id: entryId
    };
  }

  // src/front-end-module/ui.mjs
  var _a;
  var Ui = (_a = class {
    // #endregion --- class variables ---
    /** Called when `new Ui(...)` is called
     * @param {globalThis} win Either the browser global window or jsdom dom.window
     * @param {Function} [extLog] A function that returns a function for logging
     * @param {Function} [jsonHighlight] A function that returns a highlighted HTML of JSON input
     */
    constructor(win, extLog, jsonHighlight) {
      // #region --- Class variables ---
      __publicField(this, "version", "7.5.0-src");
      // List of tags and attributes not in sanitise defaults but allowed in uibuilder.
      __publicField(this, "sanitiseExtraTags", ["uib-var"]);
      __publicField(this, "sanitiseExtraAttribs", ["variable", "report", "undefined"]);
      /** Optional Markdown-IT Plugins */
      __publicField(this, "ui_md_plugins");
      if (win) _a.win = win;
      else {
        throw new Error("Ui:constructor. Current environment does not include `window`, UI functions cannot be used.");
      }
      _a.doc = _a.win.document;
      if (extLog) _a.log = extLog;
      else _a.log = function() {
        return function() {
        };
      };
      if (jsonHighlight) this.syntaxHighlight = jsonHighlight;
      else this.syntaxHighlight = function() {
      };
      if (_a.win["markdownit"]) {
        _a.mdOpts = {
          html: true,
          xhtmlOut: false,
          linkify: true,
          _highlight: true,
          _strict: false,
          _view: "html",
          langPrefix: "language-",
          // NB: the highlightjs (hljs) library must be loaded before markdown-it for this to work
          highlight: function(str, lang) {
            if (lang && window["hljs"] && window["hljs"].getLanguage(lang)) {
              try {
                return '<pre class="">\n                                    <code class="hljs border">'.concat(window["hljs"].highlight(str, { language: lang, ignoreIllegals: true }).value, "</code></pre>");
              } finally {
              }
            }
            return '<pre class="hljs border"><code>'.concat(_a.md.utils.escapeHtml(str).trim(), "</code></pre>");
          }
        };
        _a.md = _a.win["markdownit"](_a.mdOpts);
      }
    }
    // #region ---- Internal Methods ----
    _markDownIt() {
      if (!_a.win["markdownit"]) return;
      if (!this.ui_md_plugins && _a.win["uibuilder"] && _a.win["uibuilder"].ui_md_plugins) this.ui_md_plugins = _a.win["uibuilder"].ui_md_plugins;
      _a.mdOpts = {
        html: true,
        xhtmlOut: false,
        linkify: true,
        _highlight: true,
        _strict: false,
        _view: "html",
        langPrefix: "language-",
        // NB: the highlightjs (hljs) library must be loaded before markdown-it for this to work
        highlight: function(str, lang) {
          if (window["hljs"]) {
            if (lang && window["hljs"].getLanguage(lang)) {
              try {
                return '<pre><code class="hljs border language-'.concat(lang, '" data-language="').concat(lang, '" title="Source language: \'').concat(lang, "'\">").concat(window["hljs"].highlight(str, { language: lang, ignoreIllegals: true }).value, "</code></pre>");
              } finally {
              }
            } else {
              try {
                const high = window["hljs"].highlightAuto(str);
                return '<pre><code class="hljs border language-'.concat(high.language, '" data-language="').concat(high.language, '" title="Source language estimated by HighlightJS: \'').concat(high.language, "'\">").concat(high.value, "</code></pre>");
              } finally {
              }
            }
          }
          return '<pre><code class="border">'.concat(_a.md.utils.escapeHtml(str).trim(), "</code></pre>");
        }
      };
      _a.md = _a.win["markdownit"](_a.mdOpts);
      if (this.ui_md_plugins) {
        if (!Array.isArray(this.ui_md_plugins)) {
          _a.log("error", "Ui:_markDownIt:plugins", "Could not load plugins, ui_md_plugins is not an array")();
          return;
        }
        this.ui_md_plugins.forEach((plugin) => {
          if (typeof plugin === "string") {
            _a.md.use(_a.win[plugin]);
          } else {
            const name2 = Object.keys(plugin)[0];
            _a.md.use(_a.win[name2], plugin[name2]);
          }
        });
      }
    }
    /** Show a browser notification if the browser and the user allows it
     * @param {object} config Notification config data
     * @returns {Promise} Resolves on close or click event, returns the event.
     */
    _showNotification(config) {
      if (config.topic && !config.title) config.title = config.topic;
      if (!config.title) config.title = "uibuilder notification";
      if (config.payload && !config.body) config.body = config.payload;
      if (!config.body) config.body = " No message given.";
      try {
        const notify = new Notification(config.title, config);
        return new Promise((resolve, reject) => {
          notify.addEventListener("close", (ev) => {
            ev.currentTarget.userAction = "close";
            resolve(ev);
          });
          notify.addEventListener("click", (ev) => {
            ev.currentTarget.userAction = "click";
            resolve(ev);
          });
          notify.addEventListener("error", (ev) => {
            ev.currentTarget.userAction = "error";
            reject(ev);
          });
        });
      } catch (e) {
        return Promise.reject(new Error("Browser refused to create a Notification"));
      }
    }
    // Vue dynamic inserts Don't really work ...
    // _uiAddVue(ui, isRecurse) {
    //     // must be Vue
    //     // must have only 1 root element
    //     const compToAdd = ui.components[0]
    //     const newEl = Ui.doc.createElement(compToAdd.type)
    //     if (!compToAdd.slot && ui.payload) compToAdd.slot = ui.payload
    //     this._uiComposeComponent(newEl, compToAdd)
    //     // If nested components, go again - but don't pass payload to sub-components
    //     if (compToAdd.components) {
    //         this._uiExtendEl(newEl, compToAdd.components)
    //     }
    //     console.log('MAGIC: ', this.magick, newEl, newEl.outerHTML)()
    //     this.set('magick', newEl.outerHTML)
    //     // if (compToAdd.id) newEl.setAttribute('ref', compToAdd.id)
    //     // if (elParent.id) newEl.setAttribute('data-parent', elParent.id)
    // }
    // TODO Add check if ID already exists
    // TODO Allow single add without using components array
    /** Handle incoming msg._ui add requests
     * @param {*} ui Standardised msg._ui property object. Note that payload and topic are appended to this object
     * @param {boolean} isRecurse Is this a recursive call?
     */
    _uiAdd(ui, isRecurse) {
      _a.log("trace", "Ui:_uiManager:add", "Starting _uiAdd")();
      ui.components.forEach((compToAdd, i) => {
        _a.log("trace", "Ui:_uiAdd:components-forEach:".concat(i), "Component to add: ", compToAdd)();
        let newEl;
        switch (compToAdd.type) {
          // If trying to insert raw html, wrap in a div
          case "html": {
            compToAdd.ns = "html";
            newEl = _a.doc.createElement("div");
            break;
          }
          // If trying to insert raw svg, need to create in namespace
          case "svg": {
            compToAdd.ns = "svg";
            newEl = _a.doc.createElementNS("http://www.w3.org/2000/svg", "svg");
            break;
          }
          default: {
            compToAdd.ns = "dom";
            newEl = _a.doc.createElement(compToAdd.type);
            break;
          }
        }
        if (!compToAdd.slot && ui.payload) compToAdd.slot = ui.payload;
        this._uiComposeComponent(newEl, compToAdd);
        let elParent;
        if (compToAdd.parentEl) {
          elParent = compToAdd.parentEl;
        } else if (ui.parentEl) {
          elParent = ui.parentEl;
        } else if (compToAdd.parent) {
          elParent = _a.doc.querySelector(compToAdd.parent);
        } else if (ui.parent) {
          elParent = _a.doc.querySelector(ui.parent);
        }
        if (!elParent) {
          _a.log("info", "Ui:_uiAdd", "No parent found, adding to body")();
          elParent = _a.doc.querySelector("body");
        }
        if (compToAdd.position && compToAdd.position === "first") {
          elParent.insertBefore(newEl, elParent.firstChild);
        } else if (compToAdd.position && Number.isInteger(Number(compToAdd.position))) {
          elParent.insertBefore(newEl, elParent.children[compToAdd.position]);
        } else {
          elParent.appendChild(newEl);
        }
        if (compToAdd.components) {
          this._uiExtendEl(newEl, compToAdd.components, compToAdd.ns);
        }
      });
    }
    // --- end of _uiAdd ---
    /** Enhance an HTML element that is being composed with ui data
     *  such as ID, attribs, event handlers, custom props, etc.
     * @param {*} el HTML Element to enhance
     * @param {*} comp Individual uibuilder ui component spec
     */
    _uiComposeComponent(el, comp) {
      if (comp.attributes) {
        Object.keys(comp.attributes).forEach((attrib) => {
          if (attrib === "class" && Array.isArray(comp.attributes[attrib])) comp.attributes[attrib].join(" ");
          _a.log("trace", "_uiComposeComponent:attributes-forEach", "Attribute: '".concat(attrib, "', value: '").concat(comp.attributes[attrib], "'"))();
          if (attrib === "value") el.value = comp.attributes[attrib];
          if (attrib.startsWith("xlink:")) el.setAttributeNS("http://www.w3.org/1999/xlink", attrib, comp.attributes[attrib]);
          else el.setAttribute(attrib, comp.attributes[attrib]);
        });
      }
      if (comp.id) el.setAttribute("id", comp.id);
      if (comp.type === "svg") {
        el.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.w3.org/2000/svg");
        el.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
      }
      if (comp.events) {
        Object.keys(comp.events).forEach((type) => {
          if (type.toLowerCase === "onclick") type = "click";
          try {
            el.addEventListener(type, (evt) => {
              new Function("evt", "".concat(comp.events[type], "(evt)"))(evt);
            });
          } catch (err) {
            _a.log("error", "Ui:_uiComposeComponent", "Add event '".concat(type, "' for element '").concat(comp.type, "': Cannot add event handler. ").concat(err.message))();
          }
        });
      }
      if (comp.properties) {
        Object.keys(comp.properties).forEach((prop) => {
          el[prop] = comp.properties[prop];
          if (["value", "checked"].includes(prop)) {
            el.dispatchEvent(new Event("input"));
            el.dispatchEvent(new Event("change"));
          }
        });
      }
      if (comp.slot) {
        this.replaceSlot(el, comp.slot);
      }
      if (comp.slotMarkdown) {
        this.replaceSlotMarkdown(el, comp);
      }
    }
    /** Extend an HTML Element with appended elements using ui components
     * NOTE: This fn follows a strict hierarchy of added components.
     * @param {HTMLElement} parentEl The parent HTML Element we want to append to
     * @param {*} components The ui component(s) we want to add
     * @param {string} [ns] Optional. The namespace to use.
     */
    _uiExtendEl(parentEl, components, ns = "") {
      components.forEach((compToAdd, i) => {
        _a.log("trace", "Ui:_uiExtendEl:components-forEach:".concat(i), compToAdd)();
        let newEl;
        compToAdd.ns = ns;
        if (compToAdd.ns === "html") {
          newEl = parentEl;
          this.replaceSlot(parentEl, compToAdd.slot);
        } else if (compToAdd.ns === "svg") {
          newEl = _a.doc.createElementNS("http://www.w3.org/2000/svg", compToAdd.type);
          this._uiComposeComponent(newEl, compToAdd);
          parentEl.appendChild(newEl);
        } else {
          newEl = _a.doc.createElement(compToAdd.type === "html" ? "div" : compToAdd.type);
          this._uiComposeComponent(newEl, compToAdd);
          parentEl.appendChild(newEl);
        }
        if (compToAdd.components) {
          this._uiExtendEl(newEl, compToAdd.components, compToAdd.ns);
        }
      });
    }
    // TODO Add more error handling and parameter validation
    /** Handle incoming _ui load requests
     * Can load JavaScript modules, JavaScript scripts and CSS.
     * @param {*} ui Standardised msg._ui property object. Note that payload and topic are appended to this object
     */
    _uiLoad(ui) {
      if (ui.components) {
        if (!Array.isArray(ui.components)) ui.components = [ui.components];
        ui.components.forEach(async (component) => {
          Promise.resolve().then(() => __toESM(__require(component)));
        });
      }
      if (ui.srcScripts) {
        if (!Array.isArray(ui.srcScripts)) ui.srcScripts = [ui.srcScripts];
        ui.srcScripts.forEach((script) => {
          this.loadScriptSrc(script);
        });
      }
      if (ui.txtScripts) {
        if (!Array.isArray(ui.txtScripts)) ui.txtScripts = [ui.txtScripts];
        this.loadScriptTxt(ui.txtScripts.join("\n"));
      }
      if (ui.srcStyles) {
        if (!Array.isArray(ui.srcStyles)) ui.srcStyles = [ui.srcStyles];
        ui.srcStyles.forEach((sheet) => {
          this.loadStyleSrc(sheet);
        });
      }
      if (ui.txtStyles) {
        if (!Array.isArray(ui.txtStyles)) ui.txtStyles = [ui.txtStyles];
        this.loadStyleTxt(ui.txtStyles.join("\n"));
      }
    }
    // --- end of _uiLoad ---
    /** Handle incoming _ui messages and loaded UI JSON files
     * Called from start()
     * @param {*} msg Standardised msg object containing a _ui property object
     */
    _uiManager(msg) {
      if (!msg._ui) return;
      if (!Array.isArray(msg._ui)) msg._ui = [msg._ui];
      msg._ui.forEach((ui, i) => {
        if (ui.mode && !ui.method) ui.method = ui.mode;
        if (!ui.method) {
          _a.log("error", "Ui:_uiManager", "No method defined for msg._ui[".concat(i, "]. Ignoring. "), ui)();
          return;
        }
        ui.payload = msg.payload;
        ui.topic = msg.topic;
        switch (ui.method) {
          case "add": {
            this._uiAdd(ui, false);
            break;
          }
          case "remove": {
            this._uiRemove(ui, false);
            break;
          }
          case "removeAll": {
            this._uiRemove(ui, true);
            break;
          }
          case "replace": {
            this._uiReplace(ui);
            break;
          }
          case "update": {
            this._uiUpdate(ui);
            break;
          }
          case "load": {
            this._uiLoad(ui);
            break;
          }
          case "reload": {
            this._uiReload();
            break;
          }
          case "notify": {
            this.showDialog("notify", ui, msg);
            break;
          }
          case "alert": {
            this.showDialog("alert", ui, msg);
            break;
          }
          default: {
            _a.log("error", "Ui:_uiManager", "Invalid msg._ui[".concat(i, "].method (").concat(ui.method, "). Ignoring"))();
            break;
          }
        }
      });
    }
    // --- end of _uiManager ---
    /** Handle a reload request */
    _uiReload() {
      _a.log("trace", "Ui:uiManager:reload", "reloading")();
      location.reload();
    }
    // TODO Add better tests for failures (see comments)
    /** Handle incoming _ui remove requests
     * @param {*} ui Standardised msg._ui property object. Note that payload and topic are appended to this object
     * @param {boolean} all Optional, default=false. If true, will remove ALL found elements, otherwise only the 1st is removed
     */
    _uiRemove(ui, all = false) {
      ui.components.forEach((compToRemove) => {
        let els;
        if (all !== true) els = [_a.doc.querySelector(compToRemove)];
        else els = _a.doc.querySelectorAll(compToRemove);
        els.forEach((el) => {
          try {
            el.remove();
          } catch (err) {
            _a.log("trace", "Ui:_uiRemove", "Could not remove. ".concat(err.message))();
          }
        });
      });
    }
    // --- end of _uiRemove ---
    /** Handle incoming _ui replace requests
     * @param {*} ui Standardised msg._ui property object. Note that payload and topic are appended to this object
     */
    _uiReplace(ui) {
      _a.log("trace", "Ui:_uiReplace", "Starting")();
      ui.components.forEach((compToReplace, i) => {
        _a.log("trace", "Ui:_uiReplace:components-forEach:".concat(i), "Component to replace: ", compToReplace)();
        let elToReplace;
        if (compToReplace.id) {
          elToReplace = _a.doc.getElementById(compToReplace.id);
        } else if (compToReplace.selector || compToReplace.select) {
          elToReplace = _a.doc.querySelector(compToReplace.selector);
        } else if (compToReplace.name) {
          elToReplace = _a.doc.querySelector('[name="'.concat(compToReplace.name, '"]'));
        } else if (compToReplace.type) {
          elToReplace = _a.doc.querySelector(compToReplace.type);
        }
        _a.log("trace", "Ui:_uiReplace:components-forEach:".concat(i), "Element to replace: ", elToReplace)();
        if (elToReplace === void 0 || elToReplace === null) {
          _a.log("trace", "Ui:_uiReplace:components-forEach:".concat(i, ":noReplace"), "Cannot find the DOM element. Adding instead.", compToReplace)();
          this._uiAdd({ components: [compToReplace] }, false);
          return;
        }
        let newEl;
        switch (compToReplace.type) {
          // If trying to insert raw html, wrap in a div
          case "html": {
            compToReplace.ns = "html";
            newEl = _a.doc.createElement("div");
            break;
          }
          // If trying to insert raw svg, need to create in namespace
          case "svg": {
            compToReplace.ns = "svg";
            newEl = _a.doc.createElementNS("http://www.w3.org/2000/svg", "svg");
            break;
          }
          default: {
            compToReplace.ns = "dom";
            newEl = _a.doc.createElement(compToReplace.type);
            break;
          }
        }
        this._uiComposeComponent(newEl, compToReplace);
        elToReplace.replaceWith(newEl);
        if (compToReplace.components) {
          this._uiExtendEl(newEl, compToReplace.components, compToReplace.ns);
        }
      });
    }
    // --- end of _uiReplace ---
    // TODO Allow single add without using components array
    // TODO Allow sub-components
    // TODO Add multi-slot capability
    /** Handle incoming _ui update requests
     * @param {*} ui Standardised msg._ui property object. Note that payload and topic are appended to this object
     */
    _uiUpdate(ui) {
      _a.log("trace", "UI:_uiUpdate:update", "Starting _uiUpdate", ui)();
      if (!ui.components) ui.components = [Object.assign({}, ui)];
      ui.components.forEach((compToUpd, i) => {
        _a.log("trace", "_uiUpdate:components-forEach", "Start loop #".concat(i), compToUpd)();
        let elToUpd;
        if (compToUpd.parentEl) {
          elToUpd = compToUpd.parentEl;
        } else if (compToUpd.id) {
          elToUpd = _a.doc.querySelectorAll("#".concat(compToUpd.id));
        } else if (compToUpd.selector || compToUpd.select) {
          elToUpd = _a.doc.querySelectorAll(compToUpd.selector);
        } else if (compToUpd.name) {
          elToUpd = _a.doc.querySelectorAll('[name="'.concat(compToUpd.name, '"]'));
        } else if (compToUpd.type) {
          elToUpd = _a.doc.querySelectorAll(compToUpd.type);
        }
        if (elToUpd === void 0 || elToUpd.length < 1) {
          _a.log("warn", "Ui:_uiManager:update", "Cannot find the DOM element. Ignoring.", compToUpd)();
          return;
        }
        _a.log("trace", "_uiUpdate:components-forEach", "Element(s) to update. Count: ".concat(elToUpd.length), elToUpd)();
        if (!compToUpd.slot && compToUpd.payload) compToUpd.slot = compToUpd.payload;
        elToUpd.forEach((el, j) => {
          _a.log("trace", "_uiUpdate:components-forEach", "Updating element #".concat(j), el)();
          this._uiComposeComponent(el, compToUpd);
          if (compToUpd.components) {
            _a.log("trace", "_uiUpdate:nested-component", "Element #".concat(j, " - nested-component"), compToUpd, el)();
            const nc = { _ui: [] };
            compToUpd.components.forEach((nestedComp, k) => {
              const method = nestedComp.method || compToUpd.method || ui.method;
              if (nestedComp.method) delete nestedComp.method;
              if (!Array.isArray(nestedComp)) nestedComp = [nestedComp];
              _a.log("trace", "_uiUpdate:nested-component", "Element #".concat(j, " - nested-component #").concat(k), nestedComp)();
              nc._ui.push({
                method,
                parentEl: el,
                components: nestedComp
              });
            });
            _a.log("trace", "_uiUpdate:nested-component", "Element #".concat(j, " - nested-component new manager"), nc)();
            this._uiManager(nc);
          }
        });
      });
    }
    // --- end of _uiUpdate ---
    // #endregion ---- -------- ----
    // #region ---- External Methods ----
    /** Simplistic jQuery-like document CSS query selector, returns an HTML Element
     * NOTE that this fn returns the element itself. Use $$ to get the properties of 1 or more elements.
     * If the selected element is a <template>, returns the first child element.
     * type {HTMLElement}
     * @param {string} cssSelector A CSS Selector that identifies the element to return
     * @param {"el"|"text"|"html"|"attributes"|"attr"} [output] Optional. What type of output to return. Defaults to "el", the DOM element reference
     * @param {HTMLElement} [context] Optional. The context to search within. Defaults to the document. Must be a DOM element.
     * @returns {HTMLElement|string|Array|null} Selected HTML DOM element, innerText, innerHTML, attribute list or null
     */
    $(cssSelector, output, context) {
      if (!context) context = _a.doc;
      if (!output) output = "el";
      if (!context || !context.nodeType) {
        _a.log(1, "Uib:$", "Invalid context element. Must be a valid HTML element.", context)();
        return null;
      }
      let el = context.querySelector(cssSelector);
      if (!el || !el.nodeType) {
        _a.log(1, "Uib:$", "No element found or element is not an HTML element for CSS selector ".concat(cssSelector))();
        return null;
      }
      if (el.nodeName === "TEMPLATE") {
        el = el.content.firstElementChild;
        if (!el) {
          _a.log(0, "Uib:$", "Template selected for CSS selector ".concat(cssSelector, " but it is empty"))();
          return null;
        }
      }
      let out;
      try {
        switch (output.toLowerCase()) {
          case "text": {
            out = el.innerText;
            break;
          }
          case "html": {
            out = el.innerHTML;
            break;
          }
          case "attr":
          case "attributes": {
            out = {};
            for (const attr of el.attributes) {
              out[attr.name] = attr.value;
            }
            break;
          }
          default: {
            out = el;
            break;
          }
        }
      } catch (e) {
        out = el;
        _a.log(1, "Uib:$", 'Could not process output type "'.concat(output, '" for CSS selector ').concat(cssSelector, ", returned the DOM element. ").concat(e.message), e)();
      }
      return out;
    }
    /** CSS query selector that returns ALL found selections. Matches the Chromium DevTools feature of the same name.
     * NOTE that this fn returns an array showing the PROPERTIES of the elements whereas $ returns the element itself
     * @param {string} cssSelector A CSS Selector that identifies the elements to return
     * @param {HTMLElement} [context] Optional. The context to search within. Defaults to the document. Must be a DOM element.
     * @returns {HTMLElement[]} Array of DOM elements/nodes. Array is empty if selector is not found.
     */
    $$(cssSelector, context) {
      if (!context) context = _a.doc;
      if (!context || !context.nodeType) {
        _a.log(1, "Uib:$$", "Invalid context element. Must be a valid HTML element.", context)();
        return null;
      }
      return Array.from(context.querySelectorAll(cssSelector));
    }
    /** Add 1 or several class names to an element
     * @param {string|string[]} classNames Single or array of classnames
     * @param {HTMLElement} el HTML Element to add class(es) to
     */
    addClass(classNames, el) {
      if (!Array.isArray(classNames)) classNames = [classNames];
      if (el) el.classList.add(...classNames);
    }
    /** Apply a source template tag to a target html element
     * NOTES:
     * - Any attributes are only applied to the 1ST ELEMENT of the template content. Use a wrapper div if you need to apply to multiple elements.
     * - When using 'wrap' mode, the target content is placed into the template's 1ST <slot> only (if present).
     * - styles in ALL templates are accessible to all templates & impact the whole page.
     * - scripts in templates are run AT TIME OF APPLICATION (so may run multiple times).
     * - scripts in templates are applied in order of application, so variables may not yet exist if defined in subsequent templates
     * @param {string} sourceId The HTML ID of the source element
     * @param {string} targetId The HTML ID of the target element
     * @param {object} config Configuration options
     *   @param {boolean=} config.onceOnly   If true, the source will be adopted (the source is moved)
     *   @param {object=}  config.attributes A set of key:value pairs that will be applied as attributes to the 1ST ELEMENT ONLY of the target
     *   @param {'insert'|'replace'|'wrap'}  config.mode How to apply the template. Default is 'insert'. 'replace' will replace the targets innerHTML. 'wrap' is like 'replace' but will put any target content into the template's 1ST <slot> (if present).
     */
    applyTemplate(sourceId, targetId, config) {
      var _a2;
      if (!config) config = {};
      if (!config.onceOnly) config.onceOnly = false;
      if (!config.mode) config.mode = "insert";
      const template = _a.doc.getElementById(sourceId);
      if (!template || template.tagName !== "TEMPLATE") {
        _a.log("error", "Ui:applyTemplate", "Source must be a <template>. id='".concat(sourceId, "'"))();
        return;
      }
      const target = _a.doc.getElementById(targetId);
      if (!target) {
        _a.log("error", "Ui:applyTemplate", "Target not found: id='".concat(targetId, "'"))();
        return;
      }
      const targetContent = (_a2 = target.innerHTML) != null ? _a2 : "";
      if (targetContent && config.mode === "replace") {
        _a.log("warn", "Ui:applyTemplate", "Target element is not empty, content is replaced. id='".concat(targetId, "'"))();
      }
      let templateContent;
      if (config.onceOnly === true) templateContent = _a.doc.adoptNode(template.content);
      else templateContent = _a.doc.importNode(template.content, true);
      if (templateContent) {
        if (config.attributes) {
          const el = templateContent.firstElementChild;
          Object.keys(config.attributes).forEach((attrib) => {
            el.setAttribute(attrib, config.attributes[attrib]);
          });
        }
        if (config.mode === "insert") {
          target.appendChild(templateContent);
        } else if (config.mode === "replace") {
          target.innerHTML = "";
          target.appendChild(templateContent);
        } else if (config.mode === "wrap") {
          target.innerHTML = "";
          target.appendChild(templateContent);
          if (targetContent) {
            const slot = target.getElementsByTagName("slot");
            if (slot.length > 0) {
              slot[0].innerHTML = targetContent;
            }
          }
        }
      } else {
        _a.log("warn", "Ui:applyTemplate", "No valid content found in template")();
      }
    }
    /** Converts markdown text input to HTML if the Markdown-IT library is loaded
     * Otherwise simply returns the text
     * @param {string} mdText The input markdown string
     * @returns {string} HTML (if Markdown-IT library loaded and parse successful) or original text
     */
    convertMarkdown(mdText) {
      if (!mdText) return "";
      if (!_a.win["markdownit"]) return mdText;
      if (!_a.md) this._markDownIt();
      try {
        return _a.md.render(mdText.trim());
      } catch (e) {
        _a.log(0, "uibuilder:convertMarkdown", "Could not render Markdown. ".concat(e.message), e)();
        return '<p class="border error">Could not render Markdown<p>';
      }
    }
    /** Include HTML fragment, img, video, text, json, form data, pdf or anything else from an external file or API
     * Wraps the included object in a div tag.
     * PDF's, text or unknown MIME types are also wrapped in an iFrame.
     * @param {string} url The URL of the source file to include
     * @param {object} uiOptions Object containing properties recognised by the _uiReplace function. Must at least contain an id
     * param {string} uiOptions.id The HTML ID given to the wrapping DIV tag
     * param {string} uiOptions.parentSelector The CSS selector for a parent element to insert the new HTML under (defaults to 'body')
     * @returns {Promise<any>} Status
     */
    async include(url, uiOptions) {
      if (!fetch) {
        _a.log(0, "Ui:include", "Current environment does not include `fetch`, skipping.")();
        return "Current environment does not include `fetch`, skipping.";
      }
      if (!url) {
        _a.log(0, "Ui:include", "url parameter must be provided, skipping.")();
        return "url parameter must be provided, skipping.";
      }
      if (!uiOptions || !uiOptions.id) {
        _a.log(0, "Ui:include", "uiOptions parameter MUST be provided and must contain at least an `id` property, skipping.")();
        return "uiOptions parameter MUST be provided and must contain at least an `id` property, skipping.";
      }
      let response;
      try {
        response = await fetch(url);
      } catch (error) {
        _a.log(0, "Ui:include", "Fetch of file '".concat(url, "' failed. "), error.message)();
        return error.message;
      }
      if (!response.ok) {
        _a.log(0, "Ui:include", "Fetch of file '".concat(url, "' failed. Status='").concat(response.statusText, "'"))();
        return response.statusText;
      }
      const contentType = await response.headers.get("content-type");
      let type = null;
      if (contentType) {
        if (contentType.includes("text/html")) {
          type = "html";
        } else if (contentType.includes("application/json")) {
          type = "json";
        } else if (contentType.includes("multipart/form-data")) {
          type = "form";
        } else if (contentType.includes("image/")) {
          type = "image";
        } else if (contentType.includes("video/")) {
          type = "video";
        } else if (contentType.includes("application/pdf")) {
          type = "pdf";
        } else if (contentType.includes("text/plain")) {
          type = "text";
        }
      }
      let slot = "";
      let txtReturn = "Include successful";
      let data;
      switch (type) {
        case "html": {
          data = await response.text();
          slot = data;
          break;
        }
        case "json": {
          data = await response.json();
          slot = '<pre class="syntax-highlight">';
          slot += this.syntaxHighlight(data);
          slot += "</pre>";
          break;
        }
        case "form": {
          data = await response.formData();
          slot = '<pre class="syntax-highlight">';
          slot += this.syntaxHighlight(data);
          slot += "</pre>";
          break;
        }
        case "image": {
          data = await response.blob();
          slot = '<img src="'.concat(URL.createObjectURL(data), '">');
          if (_a.win["DOMPurify"]) {
            txtReturn = "Include successful. BUT DOMPurify loaded which may block its use.";
            _a.log("warn", "Ui:include:image", txtReturn)();
          }
          break;
        }
        case "video": {
          data = await response.blob();
          slot = '<video controls autoplay><source src="'.concat(URL.createObjectURL(data), '"></video>');
          if (_a.win["DOMPurify"]) {
            txtReturn = "Include successful. BUT DOMPurify loaded which may block its use.";
            _a.log("warn", "Ui:include:video", txtReturn)();
          }
          break;
        }
        case "pdf":
        case "text":
        default: {
          data = await response.blob();
          slot = '<iframe style="resize:both;width:inherit;height:inherit;" src="'.concat(URL.createObjectURL(data), '">');
          if (_a.win["DOMPurify"]) {
            txtReturn = "Include successful. BUT DOMPurify loaded which may block its use.";
            _a.log("warn", "Ui:include:".concat(type), txtReturn)();
          }
          break;
        }
      }
      uiOptions.type = "div";
      uiOptions.slot = slot;
      if (!uiOptions.parent) uiOptions.parent = "body";
      if (!uiOptions.attributes) uiOptions.attributes = { class: "included" };
      this._uiReplace({
        components: [
          uiOptions
        ]
      });
      _a.log("trace", "Ui:include:".concat(type), txtReturn)();
      return txtReturn;
    }
    // ---- End of include() ---- //
    /** Attach a new remote script to the end of HEAD synchronously
     * NOTE: It takes too long for most scripts to finish loading
     *       so this is pretty useless to work with the dynamic UI features directly.
     * @param {string} url The url to be used in the script src attribute
     */
    loadScriptSrc(url) {
      const newScript = _a.doc.createElement("script");
      newScript.src = url;
      newScript.async = false;
      _a.doc.head.appendChild(newScript);
    }
    /** Attach a new text script to the end of HEAD synchronously
     * NOTE: It takes too long for most scripts to finish loading
     *       so this is pretty useless to work with the dynamic UI features directly.
     * @param {string} textFn The text to be loaded as a script
     */
    loadScriptTxt(textFn) {
      const newScript = _a.doc.createElement("script");
      newScript.async = false;
      newScript.textContent = textFn;
      _a.doc.head.appendChild(newScript);
    }
    /** Attach a new remote stylesheet link to the end of HEAD synchronously
     * NOTE: It takes too long for most scripts to finish loading
     *       so this is pretty useless to work with the dynamic UI features directly.
     * @param {string} url The url to be used in the style link href attribute
     */
    loadStyleSrc(url) {
      const newStyle = _a.doc.createElement("link");
      newStyle.href = url;
      newStyle.rel = "stylesheet";
      newStyle.type = "text/css";
      _a.doc.head.appendChild(newStyle);
    }
    /** Attach a new text stylesheet to the end of HEAD synchronously
     * NOTE: It takes too long for most scripts to finish loading
     *       so this is pretty useless to work with the dynamic UI features directly.
     * @param {string} textFn The text to be loaded as a stylesheet
     */
    loadStyleTxt(textFn) {
      const newStyle = _a.doc.createElement("style");
      newStyle.textContent = textFn;
      _a.doc.head.appendChild(newStyle);
    }
    /** Load a dynamic UI from a JSON web reponse
     * @param {string} url URL that will return the ui JSON
     */
    loadui(url) {
      if (!fetch) {
        _a.log(0, "Ui:loadui", "Current environment does not include `fetch`, skipping.")();
        return;
      }
      if (!url) {
        _a.log(0, "Ui:loadui", "url parameter must be provided, skipping.")();
        return;
      }
      fetch(url).then((response) => {
        if (response.ok === false) {
          throw new Error("Could not load '".concat(url, "'. Status ").concat(response.status, ", Error: ").concat(response.statusText));
        }
        _a.log("trace", "Ui:loadui:then1", "Loaded '".concat(url, "'. Status ").concat(response.status, ", ").concat(response.statusText))();
        const contentType = response.headers.get("content-type");
        if (!contentType || !contentType.includes("application/json")) {
          throw new TypeError("Fetch '".concat(url, "' did not return JSON, ignoring"));
        }
        return response.json();
      }).then((data) => {
        if (data !== void 0) {
          _a.log("trace", "Ui:loadui:then2", "Parsed JSON successfully obtained")();
          this._uiManager({ _ui: data });
          return true;
        }
        return false;
      }).catch((err) => {
        _a.log("warn", "Ui:loadui:catch", "Error. ", err)();
      });
    }
    // --- end of loadui
    /** ! NOT COMPLETE Move an element from one position to another
     * @param {object} opts Options
     * @param {string} opts.sourceSelector Required, CSS Selector that identifies the element to be moved
     * @param {string} opts.targetSelector Required, CSS Selector that identifies the element to be moved
     */
    moveElement(opts) {
      const { sourceSelector, targetSelector, moveType, position } = opts;
      const sourceEl = document.querySelector(sourceSelector);
      if (!sourceEl) {
        _a.log(0, "Ui:moveElement", "Source element not found")();
        return;
      }
      const targetEl = document.querySelector(targetSelector);
      if (!targetEl) {
        _a.log(0, "Ui:moveElement", "Target element not found")();
        return;
      }
    }
    /** Get standard data from a DOM node.
     * @param {*} node DOM node to examine
     * @param {string} cssSelector Identify the DOM element to get data from
     * @returns {object} Standardised data object
     */
    nodeGet(node, cssSelector) {
      const thisOut = {
        id: node.id === "" ? void 0 : node.id,
        name: node.name,
        children: node.childNodes.length,
        type: node.nodeName,
        attributes: void 0,
        isUserInput: node.validity ? true : false,
        userInput: !node.validity ? void 0 : {
          value: node.value,
          validity: void 0,
          willValidate: node.willValidate,
          valueAsDate: node.valueAsDate,
          valueAsNumber: node.valueAsNumber,
          type: node.type
        }
      };
      if (["UL", "OL"].includes(node.nodeName)) {
        const listEntries = _a.doc.querySelectorAll("".concat(cssSelector, " li"));
        if (listEntries) {
          thisOut.list = {
            entries: listEntries.length
          };
        }
      }
      if (node.nodeName === "DL") {
        const listEntries = _a.doc.querySelectorAll("".concat(cssSelector, " dt"));
        if (listEntries) {
          thisOut.list = {
            entries: listEntries.length
          };
        }
      }
      if (node.nodeName === "TABLE") {
        const bodyEntries = _a.doc.querySelectorAll("".concat(cssSelector, " > tbody > tr"));
        const headEntries = _a.doc.querySelectorAll("".concat(cssSelector, " > thead > tr"));
        const cols = _a.doc.querySelectorAll("".concat(cssSelector, " > tbody > tr:last-child > *"));
        if (bodyEntries || headEntries || cols) {
          thisOut.table = {
            headRows: headEntries ? headEntries.length : 0,
            bodyRows: bodyEntries ? bodyEntries.length : 0,
            columns: cols ? cols.length : 0
          };
        }
      }
      if (node.nodeName !== "#text" && node.attributes && node.attributes.length > 0) {
        thisOut.attributes = {};
        for (const attrib of node.attributes) {
          if (attrib.name !== "id") {
            thisOut.attributes[attrib.name] = node.attributes[attrib.name].value;
          }
          if (attrib.name === "class") thisOut.classes = Array.from(node.classList);
        }
      }
      if (node.nodeName === "#text") {
        thisOut.text = node.textContent;
      }
      if (node.validity) thisOut.userInput.validity = {};
      for (const v in node.validity) {
        thisOut.userInput.validity[v] = node.validity[v];
      }
      return thisOut;
    }
    // --- end of nodeGet --- //
    /** Show a browser notification if possible. Returns a promise
     * Config can be a simple string, a Node-RED msg (topic as title, payload as body)
     * or a Notifications API options object + config.title string.
     * Config ref: https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification
     * @param {object|string} config Notification config object or simple message string
     * @returns {Promise} Resolves on close or click event, returns the event.
     */
    async notification(config) {
      if (typeof config === "string") {
        config = { body: config };
      }
      if (typeof Notification === "undefined") return Promise.reject(new Error("Notifications not available in this browser"));
      let permit = Notification.permission;
      if (permit === "denied") {
        return Promise.reject(new Error("Notifications not permitted by user"));
      } else if (permit === "granted") {
        return this._showNotification(config);
      }
      permit = await Notification.requestPermission();
      if (permit === "granted") {
        return this._showNotification(config);
      }
      return Promise.reject(new Error("Notifications not permitted by user"));
    }
    /** Remove All, 1 or more class names from an element
     * @param {undefined|null|""|string|string[]} classNames Single or array of classnames. If undefined, "" or null, remove all classes
     * @param {HTMLElement} el HTML Element to add class(es) to
     */
    removeClass(classNames, el) {
      if (!classNames) {
        el.removeAttribute("class");
        return;
      }
      if (!Array.isArray(classNames)) classNames = [classNames];
      if (el) el.classList.remove(...classNames);
    }
    /** Replace or add an HTML element's slot from text or an HTML string
     * WARNING: Executes <script> tags! And will process <style> tags.
     * Will use DOMPurify if that library has been loaded to window.
     * param {*} ui Single entry from the msg._ui property
     * @param {Element} el Reference to the element that we want to update
     * @param {*} slot The slot content we are trying to add/replace (defaults to empty string)
     */
    replaceSlot(el, slot) {
      if (!el) return;
      if (!slot) slot = "";
      slot = this.sanitiseHTML(slot);
      if (el.nodeName === "TEMPLATE") {
        el.innerHTML = slot;
        return;
      }
      const tempFrag = _a.doc.createRange().createContextualFragment(slot);
      const elRange = _a.doc.createRange();
      elRange.selectNodeContents(el);
      elRange.deleteContents();
      el.append(tempFrag);
    }
    /** Replace or add an HTML element's slot from a Markdown string
     * Only does something if the markdownit library has been loaded to window.
     * Will use DOMPurify if that library has been loaded to window.
     * @param {Element} el Reference to the element that we want to update
     * @param {*} component The component we are trying to add/replace
     */
    replaceSlotMarkdown(el, component) {
      if (!el) return;
      if (!component.slotMarkdown) return;
      component.slotMarkdown = this.convertMarkdown(component.slotMarkdown);
      component.slotMarkdown = this.sanitiseHTML(component.slotMarkdown);
      el.innerHTML = component.slotMarkdown;
    }
    /** Sanitise HTML to make it safe - if the DOMPurify library is loaded
     * Otherwise just returns that HTML as-is.
     * @param {string} html The input HTML string
     * @returns {string} The sanitised HTML or the original if DOMPurify not loaded
     */
    sanitiseHTML(html) {
      if (!_a.win["DOMPurify"]) return html;
      return _a.win["DOMPurify"].sanitize(html, { ADD_TAGS: this.sanitiseExtraTags, ADD_ATTR: this.sanitiseExtraAttribs });
    }
    // TODO - Allow notify to sit in corners rather than take over the screen
    /** Show a pop-over "toast" dialog or a modal alert
     * Refs: https://www.w3.org/WAI/ARIA/apg/example-index/dialog-modal/alertdialog.html,
     *       https://www.w3.org/WAI/ARIA/apg/example-index/dialog-modal/dialog.html,
     *       https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/
     * @param {"notify"|"alert"|null} type Dialog type. If null, invalid or not provided, defaults to "notify".
     * @param {object|null} ui Standardised ui data. If not provided, defaults to {noAutohide:true,modal:true,appendToast:false}
     * @param {object} [msg] msg.payload/msg.topic - only used if payload is a string. Optional.
     * @returns {HTMLDivElement|null} The toast element (which may disappear after a timeout) or null if no content
     * @example
     * Ui.showDialog('notify', { title: 'Hello', content: 'This is a notification', noAutohide: true, appendToast: true })
     * @example
     * Ui.showDialog('alert', null, msg)
     */
    showDialog(type, ui, msg) {
      if (!type || !["notify", "alert"].includes(type)) {
        type = "notify";
      }
      if (!ui) {
        ui = {
          noAutohide: true,
          modal: true,
          appendToast: false
        };
      }
      let body = "";
      if (msg.payload && typeof msg.payload === "string") body += "<div>".concat(msg.payload, "</div>");
      if (ui.content) body += "<div>".concat(ui.content, "</div>");
      if (body === "") {
        _a.log(1, "Ui:showDialog", "Toast content is blank. Not shown.")();
        return null;
      }
      let title = "";
      if (ui.title) title = ui.title;
      else if (msg.topic) title = msg.topic;
      if (ui.noAutohide) ui.noAutoHide = ui.noAutohide;
      if (ui.noAutoHide) ui.autohide = !ui.noAutoHide;
      if (ui.autoHideDelay) {
        if (!ui.autohide) ui.autohide = true;
        ui.delay = ui.autoHideDelay;
      } else ui.autoHideDelay = 1e4;
      if (!Object.prototype.hasOwnProperty.call(ui, "autohide")) ui.autohide = true;
      let content = "";
      let icon = "";
      if (type === "alert") {
        icon = '<svg viewBox="0 0 192.146 192.146"><path d="M108.186 144.372c0 7.054-4.729 12.32-12.037 12.32h-.254c-7.054 0-11.92-5.266-11.92-12.32 0-7.298 5.012-12.31 12.174-12.31s11.91 4.992 12.037 12.31zM88.44 125.301h15.447l2.951-61.298H85.46l2.98 61.298zm101.932 51.733c-2.237 3.664-6.214 5.921-10.493 5.921H12.282c-4.426 0-8.51-2.384-10.698-6.233a12.34 12.34 0 0 1 .147-12.349l84.111-149.22c2.208-3.722 6.204-5.96 10.522-5.96h.332c4.445.107 8.441 2.618 10.513 6.546l83.515 149.229c1.993 3.8 1.905 8.363-.352 12.066zm-10.493-6.4L96.354 21.454l-84.062 149.18h167.587z" /></svg>';
        ui.modal = true;
        ui.autohide = false;
      }
      content = '<div class="toast-head">'.concat(icon).concat(title, '</div><div class="toast-body">').concat(body, "</div>");
      const removeToaster = () => {
        if (!toaster) return;
        _a.doc.body.removeEventListener("keyup", toasterEventHandler);
        toaster.removeEventListener("keyup", toasterEventHandler);
        toaster.removeEventListener("touchend", toasterEventHandler);
        toaster.removeEventListener("click", toasterEventHandler);
        toaster.remove();
      };
      const removeToast = (localToast) => {
        localToast.removeEventListener("keyup", toasterEventHandler);
        localToast.removeEventListener("touchend", toasterEventHandler);
        localToast.removeEventListener("click", toasterEventHandler);
        localToast.remove();
        if (toaster && toaster.childElementCount === 0) removeToaster();
      };
      const toasterEventHandler = (evt) => {
        evt.stopPropagation();
        if (!toaster) return;
        console.log(
          "toasterEventHandler",
          evt,
          toaster.contains(evt.target),
          newToast.contains(evt.target),
          newToast === evt.target
        );
        removeToaster();
      };
      const toastEventHandler = (evt) => {
        evt.stopPropagation();
        let localToast;
        if (evt.target.classList.contains("toast")) {
          localToast = evt.target;
        } else {
          localToast = evt.target.closest(".toast");
        }
        if (!localToast) {
          _a.log(1, "Ui:showDialog", "Event target is not a (or in a) toast element, ignoring event")();
          return;
        }
        const hasInteractiveElement = !!localToast.querySelector("input, textarea, button");
        console.log(
          "toastEventHandler",
          hasInteractiveElement,
          evt,
          localToast.contains(evt.target),
          localToast === evt.target
        );
        if (hasInteractiveElement) {
          if (evt.key !== "Escape") return;
          removeToast(localToast);
        }
        removeToast(localToast);
      };
      const newToast = _a.doc.createElement("div");
      newToast.title = "Click or Esc to clear this notifcation";
      newToast.setAttribute("class", "toast ".concat(type));
      newToast.setAttribute("role", type === "alert" ? "alertdialog" : "dialog");
      newToast.dataset.modal = ui.modal;
      newToast.dataset.autohide = ui.autohide;
      newToast.dataset.autoHideDelay = ui.autoHideDelay;
      newToast.innerHTML = content;
      if (ui.appendToast === true) {
        const lastToast = Array.from(_a.doc.body.querySelectorAll(".toast")).pop();
        if (lastToast) {
          lastToast.insertAdjacentElement("afterend", newToast);
        } else {
          _a.doc.body.insertBefore(newToast, _a.doc.body.firstChild);
        }
      } else {
        _a.doc.body.insertBefore(newToast, _a.doc.body.firstChild);
      }
      newToast.addEventListener("keyup", toastEventHandler);
      newToast.addEventListener("click", toastEventHandler);
      newToast.addEventListener("touchend", toastEventHandler);
      if (ui.autohide === true) {
        setInterval(() => {
          removeToast(newToast);
        }, ui.autoHideDelay);
      }
      let toaster;
      if (ui.modal === true) {
        toaster = _a.doc.getElementById("toaster");
        if (toaster === null) {
          toaster = _a.doc.createElement("div");
          toaster.id = "toaster";
          toaster.title = "Click, touch, or ESC to clear notifcations";
          toaster.setAttribute("class", "toaster");
          toaster.setAttribute("arial-label", "Toast message");
          toaster.addEventListener("click", toasterEventHandler);
          toaster.addEventListener("touchend", toasterEventHandler);
          _a.doc.body.addEventListener("keyup", toasterEventHandler);
          _a.doc.body.insertAdjacentElement("afterbegin", toaster);
        }
      }
      return newToast;
    }
    /** Creates and displays an overlay window with customizable content and behavior
     * @param {object} options - Configuration options for the overlay
     *   @param {string} [options.content] - Main content (text or HTML) to display
     *   @param {string} [options.title] - Optional title above the main content
     *   @param {string} [options.icon] - Optional icon to display left of title (HTML or text)
     *   @param {string} [options.type] - Overlay type: 'success', 'info', 'warning', or 'error'
     *   @param {boolean} [options.showDismiss] - Whether to show dismiss button (auto-determined if not set)
     *   @param {number|null} [options.autoClose] - Auto-close delay in seconds (null for no auto-close)
     *   @param {boolean} [options.time] - Show timestamp in overlay (default: true)
     * @returns {object} Object with close() method to manually close the overlay
     */
    showOverlay(options) {
      return showOverlay(options);
    }
    /** Directly manage UI via JSON
     * @param {object} json Either an object containing {_ui: {}} or simply simple {} containing ui instructions
     */
    ui(json) {
      let msg = {};
      if (json._ui) msg = json;
      else msg._ui = json;
      this._uiManager(msg);
    }
    /** Get data from the DOM. Returns selection of useful props unless a specific prop requested.
     * @param {string} cssSelector Identify the DOM element to get data from
     * @param {string} [propName] Optional. Specific name of property to get from the element
     * @returns {Array<*>} Array of objects containing either specific requested property or a selection of useful properties
     */
    uiGet(cssSelector, propName = null) {
      const selection = (
        /** @type {NodeListOf<HTMLInputElement>} */
        _a.doc.querySelectorAll(cssSelector)
      );
      const out = [];
      selection.forEach((node) => {
        if (propName) {
          if (propName === "classes") propName = "class";
          let prop = node.getAttribute(propName);
          if (prop === void 0 || prop === null) {
            try {
              prop = node[propName];
            } catch (error) {
            }
          }
          if (prop === void 0 || prop === null) {
            if (propName.toLowerCase() === "value") out.push(node.innerText);
            else out.push("Property '".concat(propName, "' not found"));
          } else {
            const p = {};
            const cType = prop.constructor.name.toLowerCase();
            if (cType === "namednodemap") {
              for (const key of prop) {
                p[key.name] = prop[key.name].value;
              }
            } else if (!cType.includes("map")) {
              p[propName] = prop;
            } else {
              const p2 = {};
              for (const key in prop) {
                p2[key] = prop[key];
              }
            }
            if (p.class) p.classes = Array.from(node.classList);
            out.push(p);
          }
        } else {
          out.push(this.nodeGet(node, cssSelector));
        }
      });
      return out;
    }
    /** External alias for _uiComposeComponent
     * @param {*} el HTML Element to enhance
     * @param {*} comp Individual uibuilder ui component spec
     */
    uiEnhanceElement(el, comp) {
      this._uiComposeComponent(el, comp);
    }
    // #region --- table handling ---
    /** Column metadata object definition
     * @typedef columnDefinition
     * @property {number} index The column index number
     * @property {boolean} hasName Whether the column has a defined name or not
     * @property {string} title The title of the column. Shown in the table header row
     * @property {string=} name Optional. A defined column name that will be added as the `data-col-name` to all cells in the column if defined
     * @property {string|number=} key Optional. A key value (currently unused)
     * @property {"string"|"date"|"number"|"html"=} dataType FOR FUTURE USE. Optional. What type of data will this column contain?
     * @property {boolean=} editable FOR FUTURE USE. Optional. Can cells in this column be edited?
     */
    /** Directly add a table to a parent element.
     * @param {Array<object>|Array<Array>|object} data  Input data array or object. Object of objects gives named rows. Array of objects named cols. Array of arrays no naming.
     * @param {object} [opts] Build options
     *   @param {Array<columnDefinition>=} opts.cols Column metadata. If not provided will be derived from 1st row of data
     *   @param {HTMLElement|string} opts.parent Default=body. The table will be added as a child. May be an actual HTML element or a CSS Selector
     *   @param {boolean=} opts.allowHTML Optional, default=false. If true, allows HTML cell content, otherwise only allows text. Always sanitise HTML inputs
     */
    createTable(data = [], opts = { parent: "body" }) {
      if (!opts.parent) throw new Error("[ui.js:createTable] opts.parent must be provided");
      this.buildHtmlTable(data, opts);
    }
    // TODO ...
    /** Builds & returns an HTML table element from an array (or object) of objects
     * 1st row is used for columns unless you pass opts.cols to describe them.
     * If an object of objects, inner keys are used to populate th/td `data-col-name` attribs. Outer keys applied as row ID's.
     *
     * TODO
     * - Allow optional caption, heading, footers, optional collapse
     * - Multiple headings, footers
     * - colspans, rowspans
     * - multiple tbody
     *
     * @param {Array<object>|Array<Array>|object} data Input data array or object. Object of objects gives named rows. Array of objects named cols. Array of arrays no naming.
     * @param {object} opts Table options
     *   @param {Array<columnDefinition>=} opts.cols Column metadata. If not provided will be derived from 1st row of data
     *   @param {HTMLElement|string=} opts.parent If provided, the table will be added as a child instead of returned. May be an actual HTML element or a CSS Selector
     *   @param {boolean=} opts.allowHTML Optional, default=false. If true, allows HTML cell content, otherwise only allows text. Always sanitise HTML inputs
     * @returns {HTMLTableElement|HTMLParagraphElement} Output HTML Element
     */
    buildHtmlTable(data, opts = {}) {
      let rowKeys;
      const dataType = Object.prototype.toString.apply(data);
      if (dataType === "[object Array]" || dataType === "[object Object]") {
        rowKeys = Object.keys(data);
        data = Object.values(data);
      } else {
        const out = _a.doc.createElement("p");
        out.textContent = "Input data is not an array or an object, cannot create a table.";
        return out;
      }
      if (rowKeys.length > 1e3) _a.log(1, "Uib:buildHtmlTable", "Warning, data is ".concat(rowKeys.length, " rows. Anything over 1,000 can get very slow to complete."))();
      const tbl = _a.doc.createElement("table");
      const thead = _a.doc.createElement("thead");
      const headerRow = _a.doc.createElement("tr");
      if (!opts.cols) {
        if (data.length < 1) throw new Error("[ui.js:buildHtmlTable] When no opts.cols is provided, data must contain at least 1 row");
        const hasName = Object.prototype.toString.apply(data[0]) !== "[object Array]";
        headerRow.dataset.colReference = "";
        opts.cols = [];
        Object.keys(data[0]).forEach((col, i) => {
          opts.cols.push({
            index: i,
            hasName,
            name: hasName ? col : void 0,
            key: col != null ? col : i,
            title: col
          });
        });
      }
      tbl.cols = opts.cols;
      opts.cols.forEach((col) => {
        const thEl = _a.doc.createElement("th");
        thEl.textContent = col.title;
        if (col.hasName === true) thEl.dataset.colName = name;
        headerRow.appendChild(thEl);
      });
      thead.appendChild(headerRow);
      tbl.appendChild(thead);
      const tbody = _a.doc.createElement("tbody");
      tbl.appendChild(tbody);
      const rowOpts = {
        allowHTML: true,
        cols: opts.cols
        // we only want to get this once
      };
      data.forEach((item, i) => {
        if (isNaN(Number(rowKeys[i]))) rowOpts.rowId = rowKeys[i];
        else rowOpts.rowId = void 0;
        this.tblAddRow(tbl, item, rowOpts);
      });
      if (opts.parent) {
        let parentEl;
        if (typeof opts.parent === "string") {
          parentEl = _a.doc.querySelector(opts.parent);
        } else {
          parentEl = opts.parent;
        }
        try {
          parentEl.appendChild(tbl);
        } catch (e) {
          throw new Error("[ui.js:buildHtmlTable] Could not add table to parent. ".concat(e.message));
        }
        return;
      }
      return tbl;
    }
    /** Adds (or replaces) a single row in an existing table>tbody
     * NOTE: Row numbers use the rowIndex property of the row element.
     * @param {string|HTMLTableElement} tbl Either a CSS Selector for the table or a reference to the HTML Table Element
     * @param {object|Array} rowData A single row of column/cell data
     * @param {object} [options] Additional options
     *  @param {number=} options.body Optional, default=0. The tbody section to add the row to.
     *  @param {boolean=} options.allowHTML Optional, default=false. If true, allows HTML cell content, otherwise only allows text. Always sanitise HTML inputs
     *  @param {string=} options.rowId Optional. HTML element ID for the added row
     *  @param {number=} options.afterRow Optional. If provided, the new row will be added after this row number
     *  @param {number=} options.beforeRow Optional. If provided, the new row will be added before this row number. Ignored if afterRow is provided
     *  @param {number=} options.replaceRow Optional. If provided, the specified row will be REPLACED instead of added. Ignored if afterRow or beforeRow is provided
     *  @param {Array<columnDefinition>} [options.cols] Optional. Data about each column. If not provided, will be calculated from the table
     *
     * @returns {HTMLTableRowElement} Reference to the newly added row. Use the `rowIndex` prop for the row number
     */
    tblAddRow(tbl, rowData = {}, options = {}) {
      const tblType = Object.prototype.toString.apply(tbl);
      if (Object.prototype.toString.apply(options) !== "[object Object]") throw new Error("[tblAddDataRow] options must be an object");
      const dataType = Object.prototype.toString.apply(rowData);
      if (dataType !== "[object Object]" && dataType !== "[object Array]") throw new Error("[tblAddDataRow] rowData MUST be an object or an array containing column/cell data for each column");
      let tblEl;
      if (tblType === "[object HTMLTableElement]") {
        tblEl = tbl;
      } else {
        tblEl = _a.doc.querySelector(tbl);
        if (!tblEl) throw new Error('[tblAddDataRow] Table with CSS Selector "'.concat(tbl, '" not found'));
      }
      if (!options.body) options.body = 0;
      if (!("allowHTML" in options)) options.allowHTML = false;
      const tbodyEl = tblEl.getElementsByTagName("tbody")[options.body];
      if (!tbodyEl) throw new Error("[tblAddDataRow] Table must have a tbody tag, tbody section ".concat(options.body, " does not exist"));
      if (!options.cols) options.cols = this.tblGetColMeta(tblEl);
      const colMeta = options.cols;
      const rowEl = _a.doc.createElement("tr");
      if (options.rowId) rowEl.id = options.rowId;
      const cols = [];
      for (const col of colMeta) {
        const cellEl = _a.doc.createElement("td");
        cellEl.colMeta = col;
        if (col.hasName) cellEl.dataset.colName = col.name;
        cols.push(cellEl);
      }
      Object.keys(rowData).forEach((colKey, i, row) => {
        let foundEl = cols.find((col) => {
          var _a2;
          return ((_a2 = col == null ? void 0 : col.colMeta) == null ? void 0 : _a2.name) === colKey;
        });
        let foundRowData;
        if (foundEl) {
          foundRowData = rowData[colKey];
        } else {
          let numColKey = Number(colKey);
          if (isNaN(numColKey)) numColKey = i;
          if (numColKey <= cols.length - 1) {
            foundEl = cols[numColKey];
            foundRowData = Object.values(rowData)[numColKey];
          }
        }
        if (foundEl) {
          if (options.allowHTML) foundEl.innerHTML = this.sanitiseHTML(foundRowData);
          else foundEl.textContent = foundRowData;
        }
      });
      rowEl.append(...cols);
      if ("afterRow" in options) {
        const afterRow = tbodyEl.rows[options.afterRow];
        if (afterRow) return afterRow.after(rowEl);
      } else if ("beforeRow" in options) {
        const beforeRow = tbodyEl.rows[options.beforeRow];
        if (beforeRow) return beforeRow.before(rowEl);
      } else if ("replaceRow" in options) {
        const replaceRow = tbodyEl.rows[options.replaceRow];
        if (replaceRow) return replaceRow.replaceWith(rowEl);
      }
      return tbodyEl.appendChild(rowEl);
    }
    /** Add table event listener that returns the text or html content of either the full row or a single cell
     * NOTE: Assumes that the table has a `tbody` element.
     * If cells have a `data-col-name` attribute, it will be used in the output as the column name.
     * @example tblAddListener('#eltest-tbl-table', {}, myVar)
     * @example tblAddListener('#eltest-tbl-table', {eventScope: 'cell'}, myVar2)
     *
     * @param {string} tblSelector The table CSS Selector
     * @param {object} [options] Additional options. Default={}
     *   @param {"row"|"cell"=} options.eventScope Optional, default=row. Return data for either the whole row (as an object) or for the single cell clicked
     *   @param {"text"|"html"=} options.returnType Optional, default=text. Return text or html data
     *   @param {number=} options.pad Optional, default=3. Will be used to front-pad unnamed column references with zeros. e.g. 3 => "C002"/"C012"/"C342"
     *   @param {boolean=} options.send Optional, default=true. If uibuilder is present, will automatically send a message back to Node-RED.
     *   @param {string|number=} options.logLevel Optional, default=3/info. Numeric or string log level matching uibuilder's log levels.
     *   @param {string} [options.eventType] Optional, default=click. What event to listen for.
     * @param {object=} out A variable reference that will be updated with the output data upon a click event
     */
    tblAddListener(tblSelector, options = {}, out = {}) {
      const table = _a.doc.querySelector(tblSelector);
      if (!table) throw new Error('Table with CSS Selector "'.concat(tblSelector, '" not found'));
      if (typeof out !== "object") throw new Error('The "out" argument MUST be an object');
      if (!options.eventScope) options.eventScope = "row";
      if (!options.returnType) options.returnType = "text";
      if (!options.eventType) options.eventType = "click";
      if (!options.pad) options.pad = 3;
      if (!options.logLevel) options.logLevel = 2;
      if (!("send" in options)) options.send = true;
      table.querySelector("tbody").addEventListener(options.eventType, (event) => {
        Object.keys(out).forEach((key) => delete out[key]);
        const clickedRow = event.target.closest("tr");
        const clickedCell = event.target.closest("td");
        if (clickedRow) {
          out.clickType = options.eventScope;
          out.eventType = options.eventType;
          const rowIndex = out.rowIndex = clickedRow.rowIndex;
          const cellIndex = out.cellIndex = clickedCell.cellIndex + 1;
          if (clickedRow.id) out.rowId = clickedRow.id;
          if (options.eventScope === "row") {
            clickedRow.querySelectorAll("td").forEach((cell) => {
              const colName = this.tblGetCellName(cell, options.pad);
              out[colName] = options.returnType === "text" ? cell.textContent.trim() : cell.innerHTML;
            });
          } else {
            const colName = this.tblGetCellName(clickedCell, options.pad);
            out[colName] = options.returnType === "text" ? clickedCell.textContent.trim() : clickedCell.innerHTML;
          }
          _a.log(options.logLevel, "Ui:tblAddClickListener", "".concat(options.eventScope, " ").concat(options.eventType, " on row=").concat(rowIndex, ", col=").concat(cellIndex, ", data: "), out)();
          if (options.send === true && _a.win["uibuilder"]) _a.win["uibuilder"].send({
            topic: "".concat(tblSelector, " ").concat(options.eventScope, " ").concat(options.eventType),
            payload: out
          });
        }
      });
    }
    /** Find the column definition for a single column
     * @param {string|number} rowKey Key or index to use for column search
     * @param {Array<columnDefinition>=} colMeta Array of column definitions. If not provided, will need the HTML table element.
     * @param {HTMLTableElement=} tblEl If the colMeta table not provided, provide the HTML table element to do the lookup
     * @returns {columnDefinition} Column metadata object
     */
    tblFindColMeta(rowKey, colMeta, tblEl) {
      if (!colMeta && !tblEl) throw new Error("[tblFindColMeta] Either the column metadata array or the HTML table element must be provided");
      if (!colMeta && tblEl) colMeta = this.tblGetColMeta(tblEl);
      let colDef;
      if (colMeta[rowKey]) colDef = colMeta[rowKey];
      else {
        const find = colMeta.find((c) => c.name === rowKey || c.index === Number(rowKey));
        if (find) colDef = find;
      }
      return colDef;
    }
    /** Return a standardised table cell name. Either from a `data-col-name` attribute or a numeric reference like `C003`
     * @param {HTMLTableCellElement} cellEl The cell element to process
     * @param {number=} pad Optional, default=3. Will be used to front-pad unnamed column references with zeros. e.g. 3 => "C002"/"C012"/"C342"
     * @returns {string} A cell name
     */
    tblGetCellName(cellEl, pad = 3) {
      var _a2;
      return (_a2 = cellEl.getAttribute("data-col-name")) != null ? _a2 : "C".concat(String(cellEl.cellIndex + 1).padStart(pad, "0"));
    }
    /** Returns either the existing or calculated column metadata given any table
     * First checks if the data is on the `cols` custom property of the table
     * If not, then looks 1st for a row with a `data-col-reference` attribute. Then for the first TR of the thead. Then for the first TR of the table.
     * @param {HTMLTableElement} tblEl DOM table element
     * @param {object} [options] Additional options. Default={}
     *   @param {number=} options.pad Optional, default=3. Will be used to front-pad unnamed column references with zeros. e.g. 3 => "C002"/"C012"/"C342"
     * @returns {Array<columnDefinition>} Column metadata = array of column definitions
     */
    tblGetColMeta(tblEl, options = {}) {
      var _a2, _b, _c;
      if (!options.pad) options.pad = 3;
      if (tblEl.cols) return tblEl.cols;
      let cols = (_a2 = tblEl.querySelector("tr[data-col-reference]")) == null ? void 0 : _a2.children;
      if (!cols) cols = (_b = tblEl.querySelector("thead>tr:first-of-type")) == null ? void 0 : _b.children;
      if (!cols) cols = (_c = tblEl.querySelector("tr:first-of-type")) == null ? void 0 : _c.children;
      if (!cols) {
        _a.log(1, "Ui:tblGetColMeta", "No columns found in table")();
        return [];
      }
      const colData = [];
      let cellEl;
      for (cellEl of cols) {
        const hasName = !!cellEl.dataset.colName;
        const colName = cellEl.dataset.colName;
        const colIndex = cellEl.cellIndex + 1;
        const colKey = hasName ? colName : "C".concat(String(cellEl.cellIndex + 1).padStart(options.pad, "0"));
        colData.push({
          index: colIndex,
          hasName,
          name: colName,
          key: colKey,
          title: cellEl.textContent
        });
      }
      tblEl.cols = colData;
      return colData;
    }
    /** Remove a row from an existing table
     * @param {string|HTMLTableElement} tbl Either a CSS Selector for the table or a reference to the HTML Table Element
     * @param {number} rowIndex The row number to remove (1st row is 0, last row is -1)
     * @param {object} [options] Additional options
     *  @param {number=} options.body Optional, default=0. The tbody section to add the row to.
     */
    tblRemoveRow(tbl, rowIndex, options = {}) {
      const tblType = Object.prototype.toString.apply(tbl);
      if (Object.prototype.toString.apply(options) !== "[object Object]") throw new Error("[tblRemoveRow] options must be an object");
      let tblEl;
      if (tblType === "[object HTMLTableElement]") {
        tblEl = tbl;
      } else {
        tblEl = _a.doc.querySelector(tbl);
        if (!tblEl) throw new Error('[tblRemoveRow] Table with CSS Selector "'.concat(tbl, '" not found'));
      }
      if (!options.body) options.body = 0;
      const tbodyEl = tblEl.getElementsByTagName("tbody")[options.body];
      if (!tbodyEl) throw new Error("[tblAddDataRow] Table must have a tbody tag, tbody section ".concat(options.body, " does not exist"));
      tbodyEl.deleteRow(rowIndex);
    }
    // #endregion --- table handling ---
  }, /** Reference to DOM window - must be passed in the constructor
   * Allows for use of this library/class with `jsdom` in Node.JS as well as the browser.
   * @type {Window}
   */
  __publicField(_a, "win"), /** Reference to the DOM top-level window.document for convenience - set in constructor @type {Document} */
  __publicField(_a, "doc"), /** Log function - passed in constructor or will be a dummy function
   * @type {Function}
   */
  __publicField(_a, "log"), /** Options for Markdown-IT if available (set in constructor) */
  __publicField(_a, "mdOpts"), /** Reference to pre-loaded Markdown-IT library */
  __publicField(_a, "md"), _a);
  var ui_default = Ui;
})();
/**
 * @description Overlay window for displaying messages and notifications.
 * Included in the UI module and from there into the main uibuilder module.
 * @license Apache-2.0
 * @author Julian Knight (Totally Information)
 * @copyright (c) 2025-2025 Julian Knight (Totally Information)
 */
